package com.google.adwords.samples.disapproved;

import com.google.api.adwords.v10.Ad;
import com.google.api.adwords.v10.AdGroup;
import com.google.api.adwords.v10.AdGroupStatus;
import com.google.api.adwords.v10.ApiError;
import com.google.api.adwords.v10.Campaign;
import com.google.api.adwords.v10.CampaignStatus;
import com.google.api.adwords.v10.TextAd;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Find Disapproved Creatives
 * @author alecmgo@gmail.com (Alec Go)
 *
 */
public class DisapprovedCreatives {
  
  private final static Logger logger = Logger.getLogger(DisapprovedCreatives.class.getName());
  
  private final String OUTPUT_FILE = "creativesDump.tsv";
  
  private FileWriter writer;
  
  public static void main(String[] args) throws Exception {
    String account = "";
    int continueAfter = 0;
    
    //Optional account parameter, if it is different from what is specified in the properties file
    if(args.length > 0) {
      account = args[0];
    }

    //Optional "continuation" parameter
    if(args.length > 1) {
      continueAfter = Integer.parseInt(args[1]);
    }
    
    DisapprovedCreatives d = new DisapprovedCreatives();
    d.run(account, continueAfter);
  }
  
  /**
   * Dumps the disapproved creatives under an MCC account. This function can be
   * run from the middle of an MCC account, in case the script stops for any
   * reason. More can be found in the Readme.
   * 
   * @param mccAccount Accounts under this MCC account will be dumped
   * @param continueFrom Continue from this # account
   * @throws AppException
   */
  private void run(String mccAccount, int continueFrom) throws AppException {
    AdWordsInterface adwordsInterface = new AdWordsInterface(mccAccount);
    String[] accounts = adwordsInterface.getClientAccounts();
    try {
      writer = new FileWriter(new File(OUTPUT_FILE));
      writer.write(Constants.CREATIVE_REPORT_HEADER_LINE);
      writer.flush();
      for (int i = 0; i < accounts.length; i++) {
        if (i >= continueFrom) {
          logger.info(i + ". Analyzing: " + accounts[i]);
          dumpAccount(accounts[i]);
        } else {
          logger.info(i + ". Skipping account: " + accounts[i]);
        }
      }
      writer.close();
    } catch (IOException e) {
      throw new AppException(e);
    }
  }
  
  /**
   * Dumps an account's creatives.  The basic algorithms is this:
   * 1. For the given account, get the campaigns
   * 2. For each campaigns, get the adgroups,
   * 3. For each adgroup, get all the creatives
   * 4. Dump the creatives into the writer parameter
   * @param account
   * @throws AppException
   */
  private void dumpAccount(String account) throws AppException {
    logger.info("Dumping account: " + account);
    String outputLine = account + "\t";
    
    AdWordsInterface adwordsInterface = new AdWordsInterface(account);
    Campaign[] campaigns = adwordsInterface.getAllAdWordsCampaigns();
    if (campaigns != null) {
      dumpCampaigns(campaigns, adwordsInterface, outputLine);
    }
  }
  
  /**
   * Dumps each Campaign in the Campaign[]
   * @param campaigns
   * @throws AppException
   */
  private void dumpCampaigns(Campaign[] campaigns, AdWordsInterface adwordsInterface, String outputLine) throws AppException {
    for (Campaign campaign : campaigns) {
      if(campaign.getStatus().equals(CampaignStatus.Active)) {
        logger.info("Dumping Campaign: " + campaign.getName());
        String writeLine = outputLine + campaign.getId() + "\t" + campaign.getName() + "\t";
        AdGroup[] adGroups = adwordsInterface.getAllAdGroups(campaign.getId());
        if (adGroups != null) {
          dumpAdGroups(adGroups, adwordsInterface, writeLine);
        }
      }
    }
  }

  /**
   * Dumps the AdGroup[]. The number of AdGroups can be extremely large, so
   * these need to be split up before passing the array to the AdWords API. This
   * function splits up the AdGroups into buckets, and dumps each bucket to
   * disk.
   * 
   * @param adGroups
   * @param adwordsInterface
   * @param outputLine
   * @throws AppException
   */
  private void dumpAdGroups(AdGroup[] adGroups,
      AdWordsInterface adwordsInterface, String outputLine) throws AppException {
    if (adGroups != null) {
      List<AdGroup> enabledAdGroups = getOnlyEnabledAdGroups(adGroups);
      List<Integer[]> buckets = convertAdGroupsListToBuckets(enabledAdGroups);
      // Iterate through each AdGroup bucket
      for (Integer[] bucket : buckets) {
        Ad[] ads = adwordsInterface.getAllAds(convertIntegerToInt(bucket));
        dumpAds(ads, adwordsInterface, outputLine);
      }
    }
  }
  
  /**
   * Dump Ads and the Disapproval reason.  This function determines what fields
   * are outputted to the file.  If the outputs change, then the 
   * <code>Constants.CREATIVE_REPORT_HEADER_LINE</code> must change.
   * 
   * @param ads
   * @param outputLine
   * @throws AppException
   */
  private void dumpAds(Ad[] ads, AdWordsInterface adwordsInterface,
      String outputLine) throws AppException {
    if (ads != null) {
      Ad[] disapprovedAds = getOnlyDisapprovedCreatives(ads);
      logger.info("Number of disapproved ads: " + disapprovedAds.length);
      if (disapprovedAds.length > 0) {
        for (int i = 0; i < disapprovedAds.length; i++) {
          String writeLine =
              outputLine + disapprovedAds[i].getAdGroupId() + "\t";
          
          writeLine += disapprovedAds[i].getId() + "\t";
          writeLine += disapprovedAds[i].getStatus() + "\t";
          if (disapprovedAds[i] instanceof TextAd) {
            TextAd textAd = (TextAd) disapprovedAds[i];
            writeLine += "\"" + textAd.getHeadline() + "\"\t";
            writeLine += "\"" + textAd.getDescription1() + "\"\t";
            writeLine += "\"" + textAd.getDescription2() + "\"\t";
          }

          /*
           * Check for the reason for disapproval. This is performed on a "per
           * ad" basis because not all ads have a disapproval reason. This is
           * very slow 
           * TODO Build a cache of Ads and their disapproval reasons.
           */          
          ApiError[] apiErrors = adwordsInterface.checkAds(new Ad[]{disapprovedAds[i]});
          if (apiErrors != null) {
            ApiError apiError = apiErrors[0];
            writeLine += apiError.getCode() + "\t";
            writeLine += apiError.getDetail().replace("\n", "") + "\t";
            writeLine += apiError.isIsExemptable() + "\t";
            writeLine += apiError.getField() + "\t";
            writeLine += apiError.getIndex() + "\t";
            writeLine += apiError.getTextIndex() + "\t";
            writeLine += apiError.getTextLength() + "\t";
            writeLine += apiError.getTrigger() + "\t";
          }
          
          writeLine += "\n";
          
          try {
            writer.write(writeLine);
            writer.flush();
          } catch(IOException e) {
            throw new AppException(e);
          }
        }
      }
    }
  }  
  
  /**
   * Given an AdGroup[], return a list of only the Enabled AdGroups
   * 
   * @param adGroups
   * @return enabled adgroups
   */
  private static List<AdGroup> getOnlyEnabledAdGroups(AdGroup[] adGroups) {
    List<AdGroup> enabledAdGroups = new ArrayList<AdGroup>();
    for(AdGroup adGroup:adGroups) {
      if(adGroup.getStatus().equals(AdGroupStatus.Enabled)) {
        enabledAdGroups.add(adGroup);
      }
    }
    return enabledAdGroups;
  }
  
  
  /**
   * Converts AdGroup[] to an List<Integer[]>, which is needed for the AdService method calls
   * @param adGroups
   * @return converted adgroup list
   */
  private static List<Integer[]> convertAdGroupsListToBuckets(List<AdGroup> adGroups) {
    List<Integer[]> buckets = new ArrayList<Integer[]>();
    int numBuckets = (int) Math.ceil((new Double(adGroups.size()) / (new Double(Constants.ADGROUPS_BUCKET_SIZE))));

    int adGroupCount = 0;
    logger.fine("numBuckets: " + numBuckets);
    
    for(int r = 0; r < numBuckets; r++) {
      int bucketSize = Math.min(Constants.ADGROUPS_BUCKET_SIZE, adGroups.size() - adGroupCount);
      Integer[] bucket = new Integer[bucketSize];
      for(int c = 0; c < bucketSize; c++) {
        bucket[c] = adGroups.get(adGroupCount).getId();
        adGroupCount++;
      }
      buckets.add(bucket);
    }
    
    return buckets;
  }
  
  /**
   * Converts an Integer[] object to an int[] 
   * TODO Find a better way to do this.  There should be something better.
   * @param x
   * @return int[]
   */
  private static int[] convertIntegerToInt(Integer[] x) {
    int[] y = new int[x.length];
    for(int i = 0; i < x.length; i++) {
      y[i] = x[i];
    }
    return y;
  }
  
  /**
   * Returns a subset of Ads that are disapproved that *ARE NOT* disabled.
   * Disabled ads are deleted ads.
   * @param ads
   * @return only disapproved ads
   */
  private static Ad[] getOnlyDisapprovedCreatives(Ad[] ads) {
    List<Ad> disapprovedAds = new ArrayList<Ad>();
    for(Ad ad : ads) {
      if(ad.isDisapproved()) {
        disapprovedAds.add(ad);
      }
    }
    return disapprovedAds.toArray(new Ad[0]);
  }

}
